﻿using System;
using System.Text;

namespace PacketRush.Core
{
    /// <summary>
    /// Creates and writes bytes for a <see cref="T:PacketRush.Core.Packet"/>.
    /// </summary>
    /// <remarks>
    /// <para>
    /// When creating an instance of <c>PacketWriter</c> an underlying array of bytes
    /// is initialized to the max packet size.
    /// </para>
    /// <para>
    /// To write data with <c>PacketWriter</c> any of the <c>Write()</c> methods
    /// can be called, upon calling these methods <c>PacketWriter</c> writes to the underlying array
    /// and modifies the <see cref="PacketRush.Core.PacketWriter.Length"/> property to track how many bytes
    /// have been written.
    /// </para>
    /// <para>
    /// If the length of the bytes written exceeds the max packet size, the underlying array will be resized according
    /// to the value of <see cref="PacketRush.Core.PacketWriter.GrowSize"/>.
    /// </para>
    /// <para>
    /// To read packets, see <see cref="PacketRush.Core.PacketReader"/>.
    /// </para>
    /// </remarks>
    /// <example>
    /// The following example shows how to create an instance of <c>PacketWriter</c>, write some values 
    /// and copy the writer to a new instance of <c>Packet</c>.
    /// <code source="..\PacketRush.Core\DocExamples\PacketWriter.cs" region="Example01"></code>
    /// </example>
    /// <seealso cref="PacketRush.Core.Packet"/>
    /// <seealso cref="PacketRush.Core.PacketReader"/>
    public class PacketWriter
    {
        private byte[] _Data;
        private int _Length = 0;

        /// <summary>
        /// The current length in bytes of the data written, including the packet header size.
        /// </summary>
        public int Length
        {
            get { return _Length; }
        }
        private int _GrowSize = 64;

        /// <summary>
        /// The number of bytes the underlying byte array will grow to if more bytes are required for writing,
        /// defaults to 64.
        /// </summary>
        public int GrowSize
        {
            get { return _GrowSize; }
            set { _GrowSize = value; }
        }

        /// <summary>
        /// Create an instance of the <c>PacketWriter</c>.
        /// </summary>
        public PacketWriter()
        {
            _Data = new byte[Packet.MaxPacketSize];
            _Length = Packet.HeaderLengthInBytes;
        }

        /// <summary>
        /// Resets the length of the <c>PacketWriter</c> to the first bytes after the packet header
        /// effectively clearing any bytes previously written.
        /// </summary>
        public void Reset()
        {
            _Length = Packet.HeaderLengthInBytes;
        }

        /// <summary>
        /// Write a byte.
        /// </summary>
        /// <param name="val">The byte to write.</param>
        public void Write(byte val)
        {
            ResizeIfNecessary(1);
            _Data[_Length] = val;
            _Length++;
        }

        /// <summary>
        /// Write a bool.
        /// </summary>
        /// <param name="val">The bool to write.</param>
        public void Write(bool val)
        {
            ResizeIfNecessary(1);
            _Data[_Length] = (byte)(val ? 1 : 0);
            _Length++;
        }

        /// <summary>
        /// Write an unsigned 16-bit integer.
        /// </summary>
        /// <param name="val">The unsigned 16-bit integer to write.</param>
        public void Write(ushort val)
        {
            ResizeIfNecessary(2);
            Buffer.BlockCopy(BitConverter.GetBytes(val), 0, _Data, _Length, 2);
            _Length += 2;

        }

        /// <summary>
        /// Write a signed 16-bit integer.
        /// </summary>
        /// <param name="val">The signed 16-bit integer to write.</param>
        public void Write(short val)
        {
            ResizeIfNecessary(2);
            Buffer.BlockCopy(BitConverter.GetBytes(val), 0, _Data, _Length, 2);
            _Length += 2;
        }

        /// <summary>
        /// Write an unsigned 32-bit integer.
        /// </summary>
        /// <param name="val">The unsigned 32-bit integer to write.</param>
        public void Write(uint val)
        {
            ResizeIfNecessary(4);
            Buffer.BlockCopy(BitConverter.GetBytes(val), 0, _Data, _Length, 4);
            _Length += 4;
        }

        /// <summary>
        /// Write a signed 32-bit integer.
        /// </summary>
        /// <param name="val">The signed 32-bit integer to write.</param>
        public void Write(int val)
        {
            ResizeIfNecessary(4);
            Buffer.BlockCopy(BitConverter.GetBytes(val), 0, _Data, _Length, 4);
            _Length += 4;
        }

        /// <summary>
        /// Write an unsigned 64-bit integer.
        /// </summary>
        /// <param name="val">The unsigned 64-bit integer to write.</param>
        public void Write(ulong val)
        {
            ResizeIfNecessary(8);
            Buffer.BlockCopy(BitConverter.GetBytes(val), 0, _Data, _Length, 8);
            _Length += 8;
        }

        /// <summary>
        /// Write an signed 64-bit integer.
        /// </summary>
        /// <param name="val">The signed 64-bit integer to write.</param>
        public void Write(long val)
        {
            ResizeIfNecessary(8);
            Buffer.BlockCopy(BitConverter.GetBytes(val), 0, _Data, _Length, 8);
            _Length += 8;
        }

        /// <summary>
        /// Write a float.
        /// </summary>
        /// <param name="val">The float to write.</param>
        public void Write(float val)
        {
            ResizeIfNecessary(4);
            Buffer.BlockCopy(BitConverter.GetBytes(val), 0, _Data, _Length, 4);
            _Length += 4;
        }

        /// <summary>
        /// Write a double.
        /// </summary>
        /// <param name="val">The double to write.</param>
        public void Write(double val)
        {
            ResizeIfNecessary(8);
            Buffer.BlockCopy(BitConverter.GetBytes(val), 0, _Data, _Length, 8);
            _Length += 8;
        }

        /// <summary>
        /// Write a char.
        /// </summary>
        /// <param name="val">The char to write.</param>
        public void Write(char val)
        {
            ResizeIfNecessary(2);

            Buffer.BlockCopy(BitConverter.GetBytes(val), 0, _Data, _Length, 2);
            _Length += 2;
        }

        /// <summary>
        /// Write bytes.
        /// </summary>
        /// <param name="bytes">The bytes to write.</param>
        public void Write(byte[] bytes)
        {
            ResizeIfNecessary(bytes.Length);
            Buffer.BlockCopy(bytes, 0, _Data, _Length, bytes.Length);
            _Length += bytes.Length;
        }

        /// <summary>
        /// Write a string.
        /// </summary>
        /// <param name="val">The string to write.</param>
        /// <param name="encoding">The encoding of the string to write.</param>
        public void Write(string val, Encoding encoding)
        {
            byte[] bytes = encoding.GetBytes(val);
            Write(bytes.Length); // Write length of string before
            Write(bytes); // Write the bytes

        }

        /// <summary>
        /// Creates a copy of the bytes written returned as a <see cref="PacketRush.Core.Packet"/>.
        /// </summary>
        /// <returns>A copy of the bytes written as a <see cref="PacketRush.Core.Packet"/>.</returns>
        public Packet ToPacket()
        {
            byte[] data = GetRawPacket();

            return Packet.Create(data);
        }

        /// <summary>
        /// Creates a copy of the bytes written returned as a <see cref="PacketRush.Core.Packet"/> with some options.
        /// </summary>
        /// <param name="senderId">The sender id, see <see cref="PacketRush.Core.Packet.SenderId"/>.</param>
        /// <param name="targetId">The target id, see <see cref="PacketRush.Core.Packet.TargetId"/>.</param>
        /// <param name="serviceId">The service id, see <see cref="PacketRush.Core.Packet.ServiceId"/>.</param>
        /// <param name="packetFlags">The packet flags.</param>
        /// <returns>A copy of the bytes written as a <see cref="PacketRush.Core.Packet"/>.</returns>
        public Packet ToPacket(int senderId, int targetId, int serviceId, PacketFlags packetFlags)
        {
            byte[] data = GetRawPacket();

            Packet packet = Packet.Create(data);
            packet.SenderId = senderId;
            packet.TargetId = targetId;
            packet.ServiceId = serviceId;
            packet.PacketFlags = packetFlags;

            return packet;
        }

        /// <summary>
        /// Creates a copy of the bytes written returned as a <see cref="PacketRush.Core.Packet"/>.
        /// </summary>
        /// <param name="packetFlags">The packet flags.</param>
        /// <returns>A copy of the bytes written as a <see cref="PacketRush.Core.Packet"/>.</returns>
        public Packet ToPacket(PacketFlags packetFlags)
        {
            byte[] data = GetRawPacket();

            Packet packet = Packet.Create(data);
            packet.PacketFlags = packetFlags;

            return packet;
        }

        /// <summary>
        /// Creates a copy of the bytes written returned as a <see cref="PacketRush.Core.Packet"/>.
        /// </summary>
        /// <param name="targetId">The target id, see <see cref="PacketRush.Core.Packet.TargetId"/>.</param>
        /// <returns>A copy of the bytes written as a <see cref="PacketRush.Core.Packet"/>.</returns>
        public Packet ToPacket(int targetId)
        {
            byte[] data = GetRawPacket();

            Packet packet = Packet.Create(data);
            packet.TargetId = targetId;

            return packet;
        }

        /// <summary>
        /// Creates a copy of the bytes written returned as a <see cref="PacketRush.Core.Packet"/>.
        /// </summary>
        /// <param name="targetId">The target id, see <see cref="PacketRush.Core.Packet.TargetId"/>.</param>
        /// <param name="packetFlags">The packet flags.</param>
        /// <returns>A copy of the bytes written as a <see cref="PacketRush.Core.Packet"/>.</returns>
        public Packet ToPacket(int targetId, PacketFlags packetFlags)
        {
            byte[] data = GetRawPacket();

            Packet packet = Packet.Create(data);
            packet.TargetId = targetId;
            packet.PacketFlags = packetFlags;

            return packet;
        }

        /// <summary>
        /// Creates a copy of the bytes written returned as a <see cref="PacketRush.Core.Packet"/>.
        /// </summary>
        /// <param name="targetId">The target id, see <see cref="PacketRush.Core.Packet.TargetId"/>.</param>
        /// <param name="packetFlags">The packet flags.</param>
        /// <param name="sendToAllButTarget">When true, indicates that the packet should be sent to all connections but the connection with the id
        /// specified for <c>targetId</c> parameter.</param>
        /// <returns>A copy of the bytes written as a <see cref="PacketRush.Core.Packet"/>.</returns>
        public Packet ToPacket(int targetId, PacketFlags packetFlags, bool sendToAllButTarget)
        {
            byte[] data = GetRawPacket();

            Packet packet = Packet.Create(data);
            packet.TargetId = targetId;
            packet.PacketFlags = packetFlags;
            packet.SendToAllButTarget = sendToAllButTarget;

            return packet;
        }

        /// <summary>
        /// Creates a copy of the bytes written returned as a <see cref="PacketRush.Core.Packet"/> in raw bytes.
        /// </summary>
        /// <returns>A copy of the bytes written as a <see cref="PacketRush.Core.Packet"/> in raw bytes.</returns>
        public byte[] ToRawPacket()
        {
            byte[] data = GetRawPacket();

            return data;
        }

        /// <summary>
        /// Creates a copy of the bytes written returned as a <see cref="PacketRush.Core.Packet"/> as raw bytes with some options.
        /// </summary>
        /// <param name="senderId">The sender id, see <see cref="PacketRush.Core.Packet.SenderId"/>.</param>
        /// <param name="targetId">The target id, see <see cref="PacketRush.Core.Packet.TargetId"/>.</param>
        /// <param name="serviceId">The service id, see <see cref="PacketRush.Core.Packet.ServiceId"/>.</param>
        /// <returns>A copy of the bytes written as a <see cref="PacketRush.Core.Packet"/> as raw bytes.</returns>
        public byte[] ToRawPacket(int senderId, int targetId, int serviceId)
        {
            byte[] data = GetRawPacket();
            Packet.WriteSenderId(senderId, data);
            Packet.WriteTargetId(targetId, data);
            Packet.WriteServiceId(serviceId, data);
            return data;
        }

        /// <summary>
        /// Creates a copy of the bytes written returned as a <see cref="PacketRush.Core.Packet"/> as raw bytes.
        /// </summary>
        /// <param name="packetFlags">The packet flags.</param>
        /// <returns>A copy of the bytes written as a <see cref="PacketRush.Core.Packet"/> as raw bytes.</returns>
        public byte[] ToRawPacket(PacketFlags packetFlags)
        {
            byte[] data = GetRawPacket();
            Packet.WritePacketFlags(packetFlags, data);
            return data;
        }

        /// <summary>
        /// Creates a copy of the bytes written returned as a <see cref="PacketRush.Core.Packet"/> as raw bytes.
        /// </summary>
        /// <param name="packetFlags">The packet flags.</param>
        /// <param name="serviceId">The service id, see <see cref="PacketRush.Core.Packet.ServiceId"/>.</param>
        /// <returns>A copy of the bytes written as a <see cref="PacketRush.Core.Packet"/> as raw bytes.</returns>
        public byte[] ToRawPacket(int serviceId, PacketFlags packetFlags)
        {
            byte[] data = GetRawPacket();
            Packet.WriteServiceId(serviceId, data);
            Packet.WritePacketFlags(packetFlags, data);
            return data;
        }

        private byte[] GetRawPacket()
        {
            byte[] data = new byte[_Length];

            Buffer.BlockCopy(_Data, 0, data, 0, _Length);

            // Write the total size to the packet header (first 4 bytes)
            Buffer.BlockCopy(BitConverter.GetBytes(_Length), 0, data, 0, 4);
            return data;
        }

        /// <summary>
        /// Resizes the packet according to the <see cref="PacketRush.Core.PacketWriter.GrowSize"/> + the value of the <c>required</c> parameter,
        /// if the value of <c>required</c> exceeds the current size of the underlying byte array.
        /// </summary>
        /// <param name="required">The number of bytes required, used to determine if a resize is required.</param>
        protected void ResizeIfNecessary(int required)
        {
            if (_Data.Length < _Length + required)
            {
                byte[] newData = new byte[_Length + required + _GrowSize];
                Buffer.BlockCopy(_Data, 0, newData, 0, _Data.Length);
                _Data = newData;
            }
        }
    }
}